<?php
/*  ___________
 *_/ Пояснения \________________________________________________________________
 *
 * Реализуемая схема распределения прав доступа:
 * Пользователь -> Группы доступа -> Роли -> Права доступа к ресурсам -> Ресурсы
 * 
 * Роли, которые получает пользователь, могут зависить от рейтинга пользователя.
 * 
 * По умолчанию существует специальная группа доступа "Администраторы".  Пользо-
 * вателям входящим в неё доступны все виды операций над всеми ресурсами. Список
 * идентификаторов пользователей,  являющихся  администраторами,  задаётся через
 * параметр настроек OP_ADMIN_ID_LIST.
 * Кроме этого данная группа может быть задана и в таблице БД t_access_group с
 * символьным кодом "Administrators". Тогда все пользователи, входящие в нёё,
 * тоже получат привилегии администратора системы.
 * 
 * По умолчанию существует специальная роль "Гость",  которая присваивается всем
 * пользователям, в том числе и не прошедшим процедуру аутентификации.
 * Если эта роль задаётся через БД, то её символьный код должен быть "Guest".
 * 
 * По умолчанию существует специальная роль "Зарегистированый пользователь", ко-
 * торая  присваивается  зарегистрированным  пользователя,  прошедших  процедуру
 * аутентификацию.  Если эта роль задаётся через БД, то её символьный код должен
 * быть "Registered".
 * 
 * Когда у пользователя несколько ролей, то их права суммируются.
 * 
 * Префиксы ACL ролей: "!" - Группы доступа, "@" - Роли, "~" - Пользователи.
 * 
 * Роли могут подключаться в соответствии с рейтингом пользователя.  Для включе-
 * ния данного  функционала укажите  в настройках поле  "use_rating" с значением
 * "TRUE".
 * 
 * Привилегию доступа к ресурсу можно разрешать динамически. Для этого необходи-
 * мо в поле БД "assert" задать объект с интерфейсом Zend_Acl_Assert_Interface в
 * сериализованном виде.  Для включения данного функционала укажите в настройках
 * поле "use_assert" с значением "TRUE".
 *______________________________________________________________________________
 */
/**
 * Класс-наследник класса определения наличия прав доступа к ресурсам.
 */
class Workset_Acl extends Zend_Acl implements Workset_Acl_Options_Interface {
	/**
	 * @var array
	 */
	protected $_aOptions = array();

	/**
	 * @var boolean
	 */
	protected $_bLoadedFromDb = false;

	/**
	 * Задание параметров класса.
	 * 
	 * @param array|Zend_Config $aOptions
	 */
	public function setOptions($aOptions) {
		if (!is_array($aOptions)) {
			if ($aOptions instanceof Zend_Config) {
				$aOptions = $aOptions->toArray();
			}
			else {
				throw new Workset_Acl_Exception(__METHOD__.' run with wrong param');
			}
		}

		$this->_aOptions = array_merge($this->_aOptions,$aOptions);
	}

	/**
	 * Получение списка параметров класса или значения конкретного параметра.
	 * 
	 * @param string $sName
	 * @return mixed
	 */
	public function getOptions($sName = '') {
		return $sName
			?( isset($this->_aOptions[$sName])
				? $this->_aOptions[$sName]
				: null
			): $this->_aOptions;
	}

	/**
	 * Формирование объекта ACL из данных, хранящихся в БД.
	 */
	public function loadFromDb() {
		$aOptions = $this->getOptions();
		$this->_bLoadedFromDb = true;

		// Проверка наличия специальной группы - Администраторы.
		$sAdminGroupCode = $aOptions[self::OP_ADMIN_GROUP_CODE];

		if ($sAdminGroupCode && !$this->hasRole("!$sAdminGroupCode")) {
			$this->addRole("!$sAdminGroupCode");
		}

		if ($sAdminGroupCode && !$this->isAllowed("!$sAdminGroupCode")) {
			$this->allow("!$sAdminGroupCode",null,null);
		}

		// Формируем запрос к БД для получение списка всех ролей групп.
		$oSelect = $this->_getDbAdapter()->select();
		$oSelect->from(array('t1' => $aOptions[self::OP_TABLE_GROUP]));

		// Обработка результатов запроса к БД.
		$sField = $aOptions[self::OP_FIELD_GROUP_CODE];

		foreach ($oSelect->query() as $aRecord) {
			// Проверка наличия роли группы и добавление её, в случае необходимости.
			$sRoleCode = isset($aRecord[$sField]) && $aRecord[$sField]
				? '!'.$aRecord[$sField]
				: null;

			if ($sRoleCode && !$this->hasRole($sRoleCode)) {
				$this->addRole($sRoleCode);
			}
		}

		// Формируем запрос к БД для получение списка всех ролей.
		$oSelect = $this->_getDbAdapter()->select();
		$oSelect->from(array('t1' => $aOptions[self::OP_TABLE_ROLE]));

		// Обработка результатов запроса к БД.
		$sField = $aOptions[self::OP_FIELD_ROLE_CODE];

		foreach ($oSelect->query() as $aRecord) {
			// Проверка наличия роли и добавление её, в случае необходимости.
			$sRoleCode = isset($aRecord[$sField]) && $aRecord[$sField]
				? '@'.$aRecord[$sField]
				: null;

			if ($sRoleCode && !$this->hasRole($sRoleCode)) {
				$this->addRole($sRoleCode);
			}
		}

		/*
		// Формируем запрос к БД для получение списка всех ресурсов.
		$oSelect = $this->_getDbAdapter()->select();
		$oSelect->from(array('t1' => $aOptions[self::OP_TABLE_RESOURCE]));

		// Обработка результатов запроса к БД.
		$sField = $aOptions[self::OP_FIELD_RESOURCE_CODE];

		foreach ($oSelect->query() as $aRecord) {
			// Проверка наличия ресурса и добавление его, в случае необходимости.
			$sResourceCode = isset($aRecord[$sField]) && $aRecord[$sField]
				? $aRecord[$sField]
				: null;

			if ($sResourceCode && !$this->has($sResourceCode)) {
				$this->addResource($sResourceCode);
			}
		}
		*/

		/*
		// Формируем запрос к БД для выборки всех правил доступа ко всем ресурсам.
		$oSelect = $this->_getDbAdapter()->select();
		$aFields = array(
			$sFieldAllow = 'f11' => $aOptions[self::OP_FIELD_RULE_ALLOW],
			$sFieldPrivilege = 'f12' => $aOptions[self::OP_FIELD_RULE_PRIVILEGE],
		);

		if ($aOptions[self::OP_USE_ASSERT]) {
			$aFields[$sFieldAssert = 'f13'] = $aOptions[self::OP_FIELD_RULE_ASSERT];
		}

		$oSelect->from(array('t1' => $aOptions[self::OP_TABLE_RULE]),$aFields);
		$oSelect->join(
			array('t2' => $aOptions[self::OP_TABLE_RESOURCE]),
			"t1.{$aOptions[self::OP_FIELD_RULE_RESOURCE]}=t2.{$aOptions[self::OP_FIELD_RESOURCE_ID]}",
			array(
				$sFieldResource = 'f21' => $aOptions[self::OP_FIELD_RESOURCE_CODE],
			)
		);
		$oSelect->join(
			array('t3' => $aOptions[self::OP_TABLE_ROLE]),
			"t1.{$aOptions[self::OP_FIELD_RULE_ROLE]}=t3.{$aOptions[self::OP_FIELD_ROLE_ID]}",
			array(
				$sFieldRole = 'f31' => $aOptions[self::OP_FIELD_ROLE_CODE],
			)
		);
		*/

		/*
		// Обработка результатов запроса к БД.
		foreach ($oSelect->query() as $aRecord) {

			// Проверка наличия роли.
			$sRoleCode = isset($aRecord[$sFieldRole]) && $aRecord[$sFieldRole]
				? '@'.$aRecord[$sFieldRole]
				: null;

			if ($sRoleCode && !$this->hasRole($sRoleCode)) {
				throw new Workset_Acl_Exception("Role with code='$sRoleCode' was not find.");
			}

			// Проверка наличия ресурса.
			$sResourceCode = isset($aRecord[$sFieldResource]) && $aRecord[$sFieldResource]
				? $aRecord[$sFieldResource]
				: null;

			if ($sResourceCode && !$this->has($sResourceCode)) {
				throw new Workset_Acl_Exception("Resource with code='$sResourceCode' was not find.");
			}

			// Проверка того, что задана конкретная привилегия для данного правила.
			$sPrivilegeCode = isset($aRecord[$sFieldPrivilege]) && $aRecord[$sFieldPrivilege]
				? $aRecord[$sFieldPrivilege]
				: null;

			// Проверка наличия объекта класса с интерфейсом Zend_Acl_Assert_Interface.
			try {
				$oAssert = $aOptions[self::OP_USE_ASSERT] && isset($aRecord[$sFieldAssert]) && $aRecord[$sFieldAssert]
					?( ($oAssert = unserialize($aRecord[$sFieldAssert])) && $oAssert instanceof Zend_Acl_Assert_Interface
						? $oAssert
						: null
					): null;
			}
			catch (Exception $oException) {
				$oAssert = null;
			}

			// Добавляем правило в объект распределения прав доступа.
			if ($aRecord[$sFieldAllow] == 'Y') {
				$this->allow($sRoleCode,$sResourceCode,$sPrivilegeCode,$oAssert);
			}
			else {
				$this->deny($sRoleCode,$sResourceCode,$sPrivilegeCode,$oAssert);
			}
		}
		*/

		// Проверка наличия специальной роли - Гость.
		$sGuestCode = $aOptions[self::OP_GUEST_ROLE_CODE];

		if ($sGuestCode && !$this->hasRole("@{$sGuestCode}")) {
			$this->addRole("@{$sGuestCode}");
		}

		// Проверка наличия специальной роли - Зарегистрированый пользователь.
		$sRegisteredCode = $aOptions[self::OP_REGISTERED_ROLE_CODE];

		if ($sRegisteredCode && !$this->hasRole("@{$sRegisteredCode}")) {
			$this->addRole("@{$sRegisteredCode}");
		}
	}

	/**
	 * Добавление роли конкретного пользователя.
	 * 
	 * @param string|Zend_Acl_Role_Interface $role
	 */
	public function loadUserRole($role) {
		$aOptions = $this->getOptions();
		$aUserRoles = array('@'.$aOptions[self::OP_GUEST_ROLE_CODE]);

		$sRoleCode = $role instanceof Zend_Acl_Role_Interface
			? (string)$role->getRoleId()
			: (string)$role;

		if (0 < $iUserId = intval(substr($sRoleCode,1))) {
			$aUserRoles[] = "@{$aOptions[self::OP_REGISTERED_ROLE_CODE]}";

			// Формирование запроса к БД.
			$oSelect = $this->_getDbAdapter()->select();
			$oSelect->from(
				array('t1' => $aOptions[self::OP_TABLE_U2G]),
				array()
			);
			$oSelect->joinLeft(
				array('t2' => $aOptions[self::OP_TABLE_GROUP]),
				"t1.{$aOptions[self::OP_FIELD_U2G_GROUP]}=t2.{$aOptions[self::OP_FIELD_GROUP_ID]}",
				array($sFieldGroupCode = 'f21' => $aOptions[self::OP_FIELD_GROUP_CODE])
			);
			$oSelect->joinLeft(
				array('t3' => $aOptions[self::OP_TABLE_R2G]),
				"t2.{$aOptions[self::OP_FIELD_GROUP_ID]}=t3.{$aOptions[self::OP_FIELD_R2G_GROUP]}",
				array()
			);
			$oSelect->joinLeft(
				array('t4' => $aOptions[self::OP_TABLE_ROLE]),
				"t3.{$aOptions[self::OP_FIELD_R2G_ROLE]}=t4.{$aOptions[self::OP_FIELD_ROLE_ID]}",
				array($sFieldRoleCode = 'f41' => $aOptions[self::OP_FIELD_ROLE_CODE])
			);
			$oSelect->where("t1.{$aOptions[self::OP_FIELD_U2G_USER]} = ?",$iUserId);

			if ($aOptions[self::OP_USE_RATING]) {
				$iRating = $role instanceof Workset_Acl_Rating_Interface
					? $role->getRating()
					: 0;
				$oSelect->where("t3.{$aOptions[self::OP_FIELD_R2G_RATING]} < ?",$iRating + 1);

				$oSelect->orWhere("t1.{$aOptions[self::OP_FIELD_U2G_USER]} = ?",$iUserId);
				$oSelect->where(new Zend_Db_Expr("t3.{$aOptions[self::OP_FIELD_R2G_RATING]} IS NULL"));
			}

			// Обработка запроса к БД.
			foreach ($oSelect->query() as $aRecord) {
				foreach (array('@' => $sFieldRoleCode, '!' => $sFieldGroupCode) as $sPrefix => $sCode) {
					if ($aRecord[$sCode] && $this->hasRole($sCode = $sPrefix.$aRecord[$sCode])) {
						$aUserRoles[] = $sCode;
					}
				}
			}
		}

		// Если идентификатор пользователя найден среди администраторских...
		if (false !== array_search($iUserId,$aOptions[self::OP_ADMIN_ID_LIST])) {
			$aUserRoles[] = "!{$aOptions[self::OP_ADMIN_GROUP_CODE]}";
		}

		// Добавление пользователя в ACL.
		$this->addRole($sRoleCode,array_unique($aUserRoles));
	}

	/**
	 * Загрузка конкретного ресурса.
	 * 
	 * @param string|Zend_Acl_Resource_Interface $resource
	 */
	public function loadResource($resource)
	{
		$aOptions = $this->getOptions();
		$rulesCount = 0;

		$sResource = $resource instanceof Zend_Acl_Resource_Interface
			? (string)$resource->getResourceId()
			: (string)$resource;

		// Формируем запрос к БД для выборки всех правил доступа к данному ресурсу.
		$oSelect = $this->_getDbAdapter()->select();
		$aFields = array(
			$sFieldAllow = 'f11' => $aOptions[self::OP_FIELD_RULE_ALLOW],
		);

		if ($aOptions[self::OP_USE_ASSERT]) {
			$aFields[$sFieldAssert = 'f13'] = $aOptions[self::OP_FIELD_RULE_ASSERT];
		}

		$oSelect->from(array('t1' => $aOptions[self::OP_TABLE_RULE]),$aFields);
		$oSelect->join(
			array('t2' => $aOptions[self::OP_TABLE_RESOURCE]),
			"t1.{$aOptions[self::OP_FIELD_RULE_RESOURCE]}=t2.{$aOptions[self::OP_FIELD_RESOURCE_ID]}",
			array(
				$sFieldResource = 'f21' => $aOptions[self::OP_FIELD_RESOURCE_CODE],
				$sFieldResourceId = 'f22' => $aOptions[self::OP_FIELD_RESOURCE_ID],
			)
		);
		$oSelect->join(
			array('t3' => $aOptions[self::OP_TABLE_ROLE]),
			"t1.{$aOptions[self::OP_FIELD_RULE_ROLE]}=t3.{$aOptions[self::OP_FIELD_ROLE_ID]}",
			array(
				$sFieldRole = 'f31' => $aOptions[self::OP_FIELD_ROLE_CODE],
			)
		);
		$oSelect->join(
			array('t4' => $aOptions[self::OP_TABLE_PRIVILEGE]),
			"t1.{$aOptions[self::OP_FIELD_RULE_PRIVILEGE]}=t4.{$aOptions[self::OP_FIELD_PRIVILEGE_ID]}",
			array(
				$sFieldPrivilege = 'f41' => $aOptions[self::OP_FIELD_PRIVILEGE_CODE],
			)
		);

		// Добавление фильтрации по ресурсу.
		if ((string)(int)$sResource === (string)$sResource) {
			$oSelect->where("t1.{$aOptions[self::OP_FIELD_RULE_RESOURCE]} = ?",(int)$sResource);
		}
		else {
			$oSelect->where("t2.{$aOptions[self::OP_FIELD_RESOURCE_CODE]} = ?",(string)$sResource);
		}

		// Обработка результатов запроса к БД.
		foreach ($oSelect->query() as $aRecord) {
			$rulesCount++;

			// Проверка наличия роли.
			$sRoleCode = isset($aRecord[$sFieldRole]) && $aRecord[$sFieldRole]
				? '@'.$aRecord[$sFieldRole]
				: null;

			if ($sRoleCode && !$this->hasRole($sRoleCode)) {
				throw new Workset_Acl_Exception("Role with code='$sRoleCode' was not find.");
			}

			// Проверка наличия ресурса.
			$sResourceCode = isset($aRecord[$sFieldResource]) && $aRecord[$sFieldResource]
				? $aRecord[$sFieldResource]
				: null;

			if ($sResourceCode && !$this->has($sResourceCode)) {
				$this->add(new Zend_Acl_Resource($sResourceCode));
			}

			if ($aRecord[$sFieldResourceId] && !$this->has($aRecord[$sFieldResourceId])) {
				$this->add(new Zend_Acl_Resource($aRecord[$sFieldResourceId]));
			}

			// Проверка того, что задана конкретная привилегия для данного правила.
			$sPrivilegeCode = isset($aRecord[$sFieldPrivilege]) && $aRecord[$sFieldPrivilege]
				? $aRecord[$sFieldPrivilege]
				: null;

			// Проверка наличия объекта класса с интерфейсом Zend_Acl_Assert_Interface.
			try {
				$oAssert = $aOptions[self::OP_USE_ASSERT] && isset($aRecord[$sFieldAssert]) && $aRecord[$sFieldAssert]
					?( ($oAssert = unserialize($aRecord[$sFieldAssert])) && $oAssert instanceof Zend_Acl_Assert_Interface
						? $oAssert
						: null
					): null;
			}
			catch (Exception $oException) {
				$oAssert = null;
			}

			// Добавляем правило в объект распределения прав доступа.
			if ($aRecord[$sFieldAllow] == 'Y') {
				$this->allow($sRoleCode,$sResourceCode,$sPrivilegeCode,$oAssert);
				$this->allow($sRoleCode,$aRecord[$sFieldResourceId],$sPrivilegeCode,$oAssert);
			}
			else {
				$this->deny($sRoleCode,$sResourceCode,$sPrivilegeCode,$oAssert);
				$this->deny($sRoleCode,$aRecord[$sFieldResourceId],$sPrivilegeCode,$oAssert);
			}
		}

		// Если не найденно ни одного правила, то проверяем наличие ресурса и запрещаем ему доступ.
		if (!$rulesCount) {
			$oSelect = $this->_getDbAdapter()->select();
			$oSelect->from(array('t5' => $aOptions[self::OP_TABLE_RESOURCE]),array(
				$sFieldResourceId = 't51' => $aOptions[self::OP_FIELD_RESOURCE_ID],
				$sFieldResourceCode = 't52' => $aOptions[self::OP_FIELD_RESOURCE_CODE],
			));

			if ((string)(int)$sResource === (string)$sResource) {
				$oSelect->where("t5.{$aOptions[self::OP_FIELD_RESOURCE_ID]} = ?",(int)$sResource);
			}
			else {
				$oSelect->where("t5.{$aOptions[self::OP_FIELD_RESOURCE_CODE]} = ?",(string)$sResource);
			}

			if ($aRecord = $oSelect->query()->fetch()) {
				if ($aRecord[$sFieldResourceId]) {
				 	if (!$this->has($aRecord[$sFieldResourceId])) {
						$this->add(new Zend_Acl_Resource($aRecord[$sFieldResourceId]));
				 	}

					$this->deny(null,$aRecord[$sFieldResourceId]);
				}

				if ($aRecord[$sFieldResourceCode]) {
					if (!$this->has($aRecord[$sFieldResourceCode])) {
						$this->add(new Zend_Acl_Resource($aRecord[$sFieldResourceCode]));
					}

					$this->deny(null,$aRecord[$sFieldResourceCode]);
				}
			}
		}
	}

	/**
	 * (non-PHPdoc)
	 * @see library/Zend/Zend_Acl#isAllowed()
	 */
	public function isAllowed($role = null, $resource = null, $privilege = null) {
		$this->_lazyInitialization($role,$resource);
		return parent::isAllowed($role, $resource, $privilege);
	}

	/**
	 * (non-PHPdoc)
	 * @see library/Zend/Zend_Acl#inherits()
	 */
	public function inherits($resource, $inherit, $onlyParent = false) {
		$this->_lazyInitialization(null,$inherit);
		$this->_lazyInitialization(null,$resource);
		return parent::inherits($resource, $inherit, $onlyParent);
	}

	/**
	 * (non-PHPdoc)
	 * @see library/Zend/Zend_Acl#inheritsRole()
	 */
	public function inheritsRole($role, $inherit, $onlyParents = false) {
		$this->_lazyInitialization($inherit);
		$this->_lazyInitialization($role);
		return parent::inheritsRole($role, $inherit, $onlyParents);
	}

	/**
	 * Проведение ленивой инициализации.
	 * 
	 * @param $role
	 * @param $resource
	 */
	protected function _lazyInitialization($role = null, $resource = null) {
		// Если необходимо, то загружаем основные данные из БД.
		if (!$this->_bLoadedFromDb) {
			$this->loadFromDb();
		}

		// Получаем код ресурса.
		$sResourceCode = $resource instanceof Zend_Acl_Resource_Interface
			? (string)$resource->getResourceId()
			: (string)$resource;

		// Если запрашиваются права для незагруженного ресурса, то загружаем его.
		if ($sResourceCode && !$this->has($sResourceCode)) {
			$this->loadResource($resource);
		}

		// Получаем код роли, для которой проверяем права доступа.
		$sRoleCode = $role instanceof Zend_Acl_Role_Interface
			? (string)$role->getRoleId()
			: (string)$role;

		// Если запрашиваются права для роли-пользователь и данные ещё не загруженны.
		if ($sRoleCode && $sRoleCode[0] == '~' && !$this->hasRole($sRoleCode)) {
			$this->loadUserRole($role);
		}
	}

	/**
	 * Получение адаптера БД.
	 * 
	 * @return Zend_Db_Adapter_Abstract
	 */
	protected function _getDbAdapter() {
		return Zend_Registry::get($this->getOptions(self::OP_REGISTRY_DB));
	}
}
?>